Содержание статьи
64-битный лейбл — звучит возбуждающе, но в практическом плане это всего лишь хитрый маркетинговый трюк, скрывающий не только достоинства, но и недостатки. Нам дарованы 64-битные операнды и 64-битная адресация. Казалось бы, лишние разряды карман не тянут и если не пригодятся, то по крайней мере не помешают. Так ведь нет! С ростом разрядности увеличивается и длина машинных команд, а значит, время их загрузки/декодирования и размеры программы, поэтому для достижения не худшей производительности 64-битный процессор должен иметь более быструю память и более емкий кеш. Это раз.
64-битные целочисленные операнды становятся юзабельны только при обработке чисел порядка 2^33 + (8 589 934 592) и выше. Там, где 32-битному процессору требуется несколько тактов, 64-битный справляется за один. Но где ты видел такие числа в домашних и офисных приложениях? Не зря же инженеры из Intel пошли на сокращение разрядности АЛУ (арифметико‑логического устройства), ширина которого в Pentium 4 составляет всего 16 бит, против 32 бит в Pentium III. Это не значит, что Pentium 4 не может обрабатывать 32-разрядные числа. Может. Только он тратит на них больше времени, чем Pentium III. Но, поскольку процент подлинно 32-разрядных чисел (то есть таких, что используют свыше 16 бит) в домашних приложениях относительно невысок, производительность падает незначительно. Зато ядро содержит меньше транзисторов, выделяет меньше тепла и лучше работает на повышенной тактовой частоте — в целом эффект положительный.
64-битная разрядность… Помилуй! Адресовать 18 446 744 073 709 551 616 байт памяти не нужно даже Microsoft’у со всеми его графическими заворотами! Из 4 Гбайт адресного пространства Windows Processional и Windows Server только 2 Гбайт выделяют приложениям.
3 Гбайт выделяет лишь Windows Advanced Server, и не потому, что больше выделить невозможно! x86-процессоры с легкостью адресуют вплоть до 16 Гбайт (по 4 Гбайт на код, данные, стек и кучу), опять‑таки обходясь минимальной перестройкой операционной системы! Почему же до сих пор это не было сделано? Почему мы сидим на жалких 4 Гбайт, из которых реально доступны только два?! Да потому, что больше никому не нужно! Систему, адресующую 16 Гбайт, просто так не продашь, кого эти гигабайты интересуют? Вот 64 бита — совсем другое дело! Это освежает! Вот все вокруг них и танцуют.
Сравнивать 32- и 64-битные процессоры бессмысленно. Если 64-битный процессор на домашнем приложении оказывается быстрее, то отнюдь не за счет своей 64-битности, а благодаря совершенно независимым от нее конструктивным ухищрениям, на которых инженеры едва не разорвали себе задницы!
Впрочем, не будем о грустном. 64 бита все равно войдут в нашу жизнь. Для некоторых задач они очень даже ничего. Вот, например, криптография. 64 бита — это же 8 байт! 8-символьные пароли можно полностью уместить в один регистр, не обращаясь к памяти, что дает невероятный результат! Скорость перебора увеличивается чуть ли не на порядок! Ну так чего же мы ждем? Вперед! На штурм 64-битных вершин!

Что нам понадобится?
Нам потребуется 64-разрядная операционная система. Дотянуться до 64-битных регистров и прочих вкусностей x86-64-архитектуры можно только из специального 64-разрядного режима (long mode). Ни под реальным, ни под 32-разрядным защищенным x86-режимом они не доступы. И хотя мы покажем, как перевести процессор из реального в 64-разрядный режим, создание полнофункциональной операционной системы не входит в наши планы, а без нее никуда!
Теперь перейдем к подготовке инструментария. Как минимум нам понадобится ассемблер и отладчик. Мы будем использовать FASM. Он бесплатен, работает под Linux, Windows и MS-DOS, поддерживает x86-64 и обладает удобным синтаксисом.
Практически во все x86-64-порты Linux входит GNU Debugger, которого для наших задач вполне достаточно. Обладатели Windows могут воспользоваться Microsoft Debugger.
Обзор x86-64
За подробным описанием x86-64-архитектуры лучше всего обратиться к фирменной документации AMD64 Technology — AMD64 Architecture Programmer’s Manual Volume 1:Application Programming. Мы же ограничимся только беглым обзором основных нововведений.
Наконец‑то AMD сжалилась над нами и подарила программистам то, чего все так долго ждали. К семи регистрам общего назначения (восьми — с учетом ESP) добавилось еще восемь, в результате чего их общее количество достигло 15 (16) штук.
Старые регистры, расширенные до 64 бит, получили имена RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, RIP и RFLAGS. Новые регистры остались безымянными и просто пронумерованы от R8 до R15. Для обращения к младшим 8, 16 и 32 битам новых регистров можно использовать суффиксы b, w и d. Например, R9 — это 64-разрядный регистр, R9b — его младший байт (по аналогии с AL), а R9w — младшее слово (то же самое, что AX в EAX). Прямых наследников AH, к сожалению, не наблюдается, и для манипуляции со средней частью регистров приходится извращаться со сдвигами и математическими операциями.

Регистр, указатель команд RIP, теперь адресуется точно так же, как и все остальные регистры общего назначения. Программисты, заставшие живую PDP-11 (или ее отечественный клон — «Электронику БК», или УКНЦ), только презрительно хмыкнут: наконец‑то до разработчиков стали доходить очевидные истины, которые на всех нормальных платформах были реализованы еще неизвестно когда.
Возьмем простейший пример: загрузим в регистр AL опкод следующей машинной команды. На x86 приходится поступать так.
Загрузка опкода следующей машинной команды в классическом x86
call $ + 5 ; Запихнуть в стек адрес следующей команды и передать на нее управлениеpop ebx ; Вытолкнуть из стека адрес возвратаadd ebx, 6 ; Скорректировать адрес на размер команд pop/add/movmov al, [ebx] ; Теперь AL содержит опкод команды NOPNOP ; Команда, чей опкод мы хотим загрузить в ALЭто же умом поехать можно, пока все это напишешь! И еще здесь очень легко ошибиться в размере команд — приходится вычислять его вручную либо загромождать листинг никому не нужными метками. К тому же неизбежно затрагивается стек, что в ряде случаев нежелательно или недопустимо (особенно в защитных механизмах, нашпигованных антиотладочными приемами).
А теперь перепишем тот же самый пример на x86-64.
Загрузка опкода следующей машинной команды на x86-64
mov al,[rip] ; Загружаем опкод следующей машинной командыNOP ; Команда, чей опкод мы хотим загрузить в ALКрасота! Только следует помнить, что RIP всегда указывает на следующую, а отнюдь не текущую инструкцию! К сожалению, ни Jx RIP, ни CALL RIP не работают. Таких команд в лексиконе x86-64 просто нет.
Но это еще что! Исчезла абсолютная адресация! Если нам надо изменить содержимое ячейки памяти по конкретному адресу, на x86 мы поступаем приблизительно так:
dec byte ptr [666h] ; Уменьшить содержимое байта по адресу 666h на единицуПод x86-64 транслятор выдает ошибку ассемблирования, вынуждая нас прибегать к фиктивному базированию:
xor r9, r9 ; Обнулить регистр r9dec byte ptr [r9+666h] ; Уменьшить содержимое байта по адресу 0+666h на единицуЕсть и другие отличия от x86, но они не столь принципиальны. Важно то, что в режиме совместимости с x86 (Legacy Mode) ни 64-битные регистры, ни новые методы адресации не доступны! Никакими средствами (включая черную и белую магию) дотянуться до них нельзя, и, прежде чем что‑то сделать, необходимо перевести процессор в длинный режим (long mode), который делится на два подрежима: режим совместимости с x86 (compatibility mode) и 64-битный режим (64-bit mode). Режим совместимости предусмотрен только для того, чтобы 64-разрядная операционная система могла выполнять старые 32-битные приложения. Никакие 64-битные регистры здесь и не ночевали!
Реальная 64-битность обитает только в 64-bit long mode, о котором мы и будем говорить.
Hello, world на x86-64
Программирование под 64-битную версию Windows мало чем отличается от традиционного, только все операнды и адреса по умолчанию 64-разрядные, а параметры API-функций передаются большей частью через регистры, а не через стек. Первые четыре аргумента всех API-функций передаются в регистрах RCX, RDX, R8 и R9 (регистры перечислены в порядке следования аргументов, крайний левый аргумент помещается в RCX). А уж остальные параметры кладутся в стек. Все это называется x86-64 fast calling conversion (соглашение о быстрой передаче параметров для x86-64), подробное описание можно найти в статье The history of calling conventions, part 5 amd64. Также советую заглянуть на страничку бесплатного компилятора Free PASCAL и поднять документацию по способам вызова API.
В частности, вызов функции с пятью аргументами API_func( выглядит так:
mov dword ptr [rsp+20h], 5 ; Кладем на стек пятый слева аргументmov r9d, 4 ; Передаем четвертый слева аргументmov r8d, 3 ; Передаем третий слева аргументmov edx, 2 ; Передаем второй слева аргументmov ecx, 1 ; Передаем первый слева аргументcall API_funcСмещение пятого аргумента относительно верхушки стека требует пояснений. Почему оно равно 20h? Ведь адрес возврата занимает только 8 байт, кто же съел все остальные? Оказывается, они резервируются для первых четырех аргументов, переданных через регистры. Зарезервированные ячейки содержат неинициализированный мусор и по‑буржуйски называются spill, что переводится как затычка или потеря.
Вот минимум знаний, необходимых для выживания в мире 64-битной Windows при программировании на ассемблере. Остается разобрать самую малость: как эти самые 64-бита заполучить? Для перевода FASM’а в режим x86-64 достаточно указать директиву use64 и дальше кодить как обычно.
Ниже идет пример простейшей x86-64-программы, которая не делает ничего, только возвращает в регистре RAX значение 0.
; Сообщаем FASM’у, что мы хотим программировать на x86-64use64xor r9,r9 ; Обнуляем регистр r9mov rax,r9 ; Пересылаем в rax,r9 (можно сразу mov rax,0, но неинтересно)ret ; Выходим туда, откуда пришлиНикаких дополнительных аргументов командной строки указывать не надо, просто сказать fasm file-name.asm, и все! Через мгновение образуется файл file-name., который в hex-представлении выглядит так:
Дизассемблерный листинг простейшей 64-битной программы
4D 31 C9 xor r9, r94C 89 C8 mov rax, r9C3 retnФормально это типичный com-файл, вот только запустить его не удастся (во всяком случае, ни одна популярная ось его не съест), необходимо замутить законченный ELF или PE, в заголовке которого будет явно прописана нужная разрядность.
Начиная с версии 1.64 ассемблер FASM поддерживает специальную директиву format , автоматически формирующую 64-разрядный PE-файл (директиву use64 в этом случае указывать уже не нужно), а в каталоге EXAMPLES можно найти готовый пример PE64DEMO, в котором показано, как ее использовать на практике.
Ниже приведен пример x86-64-программы Hello, world с комментариями.
64-битное приложение Hello, world под Windows на FASM’е
; Пример 64-битного PE файла; Для его выполнения необходимо иметь Windows XP 64-bit edition; Указываем форматformat PE64 GUI; Указываем точку входаentry start; Создать кодовую секцию с атрибутами на чтение и исполнениеsection '.code' code readable executablestart: mov r9d,0 ; uType == MB_OK (кнопка по умолчанию) ; Аргументы по соглашению x86-64 ; передаются через регистры, не через стек! ; Префикс d задает регистр размером в слово, ; можно использовать и mov r9,0, но тогда ; машинный код будет на байт длиннее lea r8,[_caption] ; lpCaption передаем смещение ; Команда lea занимает всего 7 байт, ; а mov reg, offset — целых 11, так что ; lea намного более предпочтительна lea rdx,[_message] ; lpText передаем смещение выводимой строки mov rcx,0 ; hWnd передам дескриптор окна владельца ; (можно также использовать xor rcx, rcx, ; что на три байта короче) call [MessageBox] ; Вызываем функцию MessageBox mov ecx,eax ; Заносим в ecx результат возврата ; (функция ExitProcess ожидает 32-битный параметр,; можно использовать и mov rcx, rax, но это будет ; на байт длиннее) call [ExitProcess] ; Вызываем функцию ExitProcess; Создать секцию данных с атрибутами на чтение и запись; (вообще-то в данном случае атрибут на запись необязателен,; поскольку мы ничего не пишем, а только читаем)section '.data' data readable writeable _caption db 'PENUMBRA is awesome!',0 ; ASCIIZ-строка заголовка окна _message db 'Hello World!',0 ; ASCIIZ-строка, выводимая на экран; Создать секцию импорта с атрибутами на чтение и запись; (здесь атрибут на запись обязателен, поскольку при загрузке PE-файла; в секцию импорта будут записываться фактические адреса API-функций)section '.idata' import data readable writeable dd 0,0,0,RVA kernel_name,RVA kernel_table dd 0,0,0,RVA user_name,RVA user_table dd 0,0,0,0,0 ; Завершаем список двумя 64-разрядными нулями!kernel_table: ExitProcess dq RVA _ExitProcess dq 0 ; Завершаем список 64-разрядным нулем!user_table: MessageBox dq RVA _MessageBoxA dq 0kernel_name db 'KERNEL32.DLL',0user_name db 'USER32.DLL',0_ExitProcess dw 0 db 'ExitProcess',0_MessageBoxA dw 0 db 'MessageBoxA',0
Ассемблируем файл (fasm PE64DEMO.ASM) и запустим образовавшийся EXE на выполнение. Под 32-разрядной Windows он, естественно, не запустится.

Вдоволь наигравшись нашим первым x86-64 файлом, загрузим его в дизассемблер (например, в IDA Pro 4.7. Она хоть и матерится, предлагая использовать специальную 64-битную версию, но при нажатии на Yes все корректно дизассемблирует. Во всяком случае до тех пор, пока не столкнется с подлинным 64-битным адресом или операндом, который будет обрезан, в частности mov дизассемблируется как mov . Так что переход на 64-битную версию IDA все же очень желателен, тем более что начиная с IDA 4.9 она входит в базовую поставку). Посмотрим, что у нашей программы внутри?
Дизассемблерный листинг 64-битного приложения Hello, world
.code:0000000000401000 41 B9 00 00 00 00 mov r9d, 0.code:0000000000401006 4C 8D 05 F3 0F 00 00 lea r8, aPENUMBRA.code:000000000040100D 48 8D 15 03 10 00 00 lea rdx, aHelloWorld ; "Hello World!".code:0000000000401014 48 C7 C1 00 00 00 00 mov rcx, 0.code:000000000040101B FF 15 2B 20 00 00 call cs:MessageBoxA.code:0000000000401021 89 C1 mov ecx, eax.code:0000000000401023 FF 15 13 20 00 00 call cs:ExitProcessЧто ж, довольно громоздко, объемно и концептуально. Для сравнения дизассемблированный листинг аналогичного 32-разрядного файла приведен ниже. Старый x86-код в 1,6 раза короче! А ведь это только демонстрационная программа из нескольких строк! На полновесных приложениях разрыв будет только нарастать. Так что не стоит злоупотреблять 64-разрядным кодом без необходимости. Его следует использовать только там, где 64-битная арифметика и восемь дополнительных регистров действительно дают ощутимый выигрыш. Например, в математических задачах или программах для вскрытия паролей.

Дизассемблерный листинг 32-битного приложения Hello, world
code:00401000 6A 00 push 0code:00401002 68 00 20 40 00 push offset aPENUMBRAcode:00401007 68 17 20 40 00 push offset aHelloWorldcode:0040100C 6A 00 push 0code:0040100E FF 15 44 30 40 00 call ds:MessageBoxAcode:00401014 6A 00 push 0code:00401016 FF 15 3C 30 40 00 call ds:ExitProcessВ качестве заключительного упражнения перепишем наше приложение в стиле MASM, поклонников которого нужно не бить, а уважать. Никаких радикальных отличий не наблюдается:
64-битное приложение Hello, world под Windows на MASM’е
; Объявляем внешние API-функции, которые мы будем вызыватьextrn MessageBoxA: PROCextrn ExitProcess: PROC; Секция данных с атрибутами по умолчанию (чтение и запись).datamytit db 'PENUMBRA is awesome!', 0mymsg db 'Hello World!', 0; Секция кода с атрибутами по умолчанию (чтение и исполнение).codeMain:mov r9d, 0 ; uType = MB_OKlea r8, mytit ; LPCSTR lpCaptionlea rdx, mymsg ; LPCSTR lpTextmov rcx, 0 ; hWnd = HWND_DESKTOPcall MessageBoxAmov ecx, eax ; uExitCode = MessageBox(...)call ExitProcessEnd MainАссемблирование и линковка проходит так:
ml64 XXX.asm /link /subsystem:windows /defaultlib:kernel32.lib /defaultlib:user32.lib /entry:main
В результате чего образуется готовый к употреблению EXE-файл с румяной поджаренной корочкой нашего ЦП (FASM ассемблирует намного быстрее).
Примеры более сложных программ легко найти в сети. Как показывает практика, запросы типа x86-64 [ катастрофически неэффективны и гораздо лучше использовать что‑нибудь вроде mov rax.
Заключение
Вот мы и познакомились с архитектурой x86-64! Здесь действительно есть где развернуться и чему поучиться! Насколько эти знания окажутся востребованны на практике — так сразу и не скажешь. У AMD есть хорошие шансы пошатнуть рынок, но ведь и Intel не дремлет, активно продвигая собственные 64-разрядные платформы, известные под общим именем IA64, но о них как‑нибудь в другой раз.
Переход в 64-разрдяный режим
В исходниках FreeBSD можно найти файл amd64_tramp., быстро и грязно переводящий процессор в 64-разрядный режим. Откомпилировав, его можно записать в boot-сектор, загружающий нашу собственную операционную систему (ты ведь пишешь ее, правда?) или слинковать com-файл, запускаемый из реального x86-режима (для этого потребуется чистая MS-DOS безо всяких экстендеров). В общем, вариантов много.
Перевод процессора в 64-разрядный режим
//$FreeBSD: /repoman/r/ncvs/src/sys/boot/i386/libi386/amd64_tramp.S,v 1.4 2004/05/14/* * Quick and dirty trampoline to get into 64 bit (long) mode and running * with paging enabled so that we enter the kernel at its linked address. */#define MSR_EFER 0xc0000080#define EFER_LME 0x00000100#define CR4_PAE 0x00000020#define CR4_PSE 0x00000010#define CR0_PG 0x80000000/* GRRR. Deal with BTX that links us for a non-zero location */#define VPBASE 0xa000#define VTOP(x) ((x) + VPBASE) .data .p2align 12,0x40 .globl PT4PT4: .space 0x1000 .globl PT3PT3: .space 0x1000 .globl PT2PT2: .space 0x1000gdtdesc: .word gdtend - gdt .long VTOP(gdt) # low .long 0 # highgdt: .long 0 # null descriptor .long 0 .long 0x00000000 # %cs .long 0x00209800 .long 0x00000000 # %ds .long 0x00008000gdtend: .text .code32 .globl amd64_trampamd64_tramp: /* Be sure that interrupts are disabled */ cli /* Turn on EFER.LME */ movl $MSR_EFER, %ecx rdmsr orl $EFER_LME, %eax wrmsr /* Turn on PAE */ movl %cr4, %eax orl $(CR4_PAE | CR4_PSE), %eax movl %eax, %cr4 /* Set %cr3 for PT4 */ movl $VTOP(PT4), %eax movl %eax, %cr3 /* Turn on paging (implicitly sets EFER.LMA) */ movl %cr0, %eax orl $CR0_PG, %eax movl %eax, %cr0 /* Now we’re in compatibility mode. set %cs for long mode */ movl $VTOP(gdtdesc), %eax movl VTOP(entry_hi), %esi movl VTOP(entry_lo), %edi lgdt (%eax) ljmp $0x8, $VTOP(longmode) .code64longmode: /* We're still running V=P, jump to entry point */ movl %esi, %eax salq $32, %rax orq %rdi, %rax pushq %rax ret









